<?php

namespace ly;

class Cache {
	private static $ins = null;

	private $key_clear;
	private $root;
	private $dir;
	private $md5 = true;
	private $prefix = null;

	private function __construct () {
		$this->key_clear = '.clear_' . date('Ymd');
		$this->root      = self::slash_dir($_SERVER['DOCUMENT_ROOT'] === '' ? dirname(__DIR__) : $_SERVER['DOCUMENT_ROOT']);
		if (is_dir($this->root) && !is_writeable($this->root)) chmod($this->root, 0777);
		$this->dir();
	}

	/**
	 * @return self
	 */
	public static function init () {
		if (!(self::$ins instanceof Cache)) self::$ins = new self();

		return self::$ins;
	}

	/**
	 * 缓存文件存储目录 追加目录
	 *
	 * @param  string  ...$dirs
	 *
	 * @return $this
	 */
	public function dir (...$dirs) {
		$dir       = self::slash_dir($this->root, 'cache', ...$dirs);
		$this->dir = $dir;

		return $this;
	}

	/**
	 * key前缀
	 *
	 * @param  string  $prefix
	 *
	 * @return $this
	 */
	public function prefix ($prefix = null) {
		$this->prefix = $prefix;

		return $this;
	}

	/**
	 * key是否启用MD5
	 *
	 * @param  bool  $md5
	 *
	 * @return $this
	 */
	public function md5 ($md5 = true) {
		$this->md5 = $md5;

		return $this;
	}

	/**
	 * 添加或修改缓存
	 *
	 * @param  string  $key     缓存key
	 * @param  mixed   $data    缓存数据
	 * @param  int     $expire  缓存过期时间(秒)，<=0表示不过期
	 *
	 * @return bool
	 */
	public function set ($key, $data, $expire = 0) {
		if ($data === null) return false;
		if ($key === $this->key_clear && $expire <= 0) $expire = 60 * 60;

		if (!is_dir($this->dir) && !mkdir($this->dir, 0777, true)) return false;

		$name    = $this->key2name($key);
		$file    = $this->dir . $name . '.php';
		$time    = time();
		$content = "<?php" . PHP_EOL;
		$content .= sprintf('// %s -> %s', date('Y-m-d H:i:s', $time), $expire > 0 ? date('Y-m-d H:i:s', $time + $expire) : '∞') . PHP_EOL;
		$content .= PHP_EOL . 'return ' . var_export($data, true) . ';';

		if (false === file_put_contents($file, $content, LOCK_EX)) return false;

		if (!touch($file, $expire > 0 ? $time + $expire : 0)) {
			$this->del($key);

			return false;
		}

		if ($key !== $this->key_clear) $this->clear();

		return true;
	}

	/**
	 * 判断是否存在key
	 *
	 * @param  string  $key  缓存key
	 *
	 * @return bool
	 */
	public function has ($key) {
		$name = $this->key2name($key);
		$file = $this->dir . $name . '.php';
		if (is_file($file)) {
			$mtime = filemtime($file);
			if ($mtime === 0 || $mtime >= time()) return true;
			$this->del($key);
		}

		return false;
	}

	/**
	 * 读取缓存数据
	 *
	 * @param  string  $key  缓存key
	 *
	 * @return mixed|null 成功：缓存文件内的数据，失败：null
	 */
	public function get ($key) {
		$name = $this->key2name($key);
		$file = $this->dir . $name . '.php';
		if (is_file($file)) {
			$mtime = filemtime($file);
			if ($mtime === 0 || $mtime >= time()) return include $file;
			$this->del($key);
		}

		return null;
	}

	/**
	 * 删除指定缓存
	 *
	 * @param  string  ...$keys  缓存key
	 *
	 * @return int 成功删除缓存数量
	 */
	public function del (...$keys) {
		$n = 0;
		foreach ($keys as $key) {
			$name = $this->key2name($key);
			$file = $this->dir . $name . '.php';
			if (!is_file($file)) continue;
			if (@unlink($file) || (chmod($file, 0777) && @unlink($file))) $n++;
		}
		$dir = $this->dir;
		do {
			$bool = @rmdir($dir);
			$dir  = self::slash_dir(dirname($dir));
		} while ($bool && strpos($dir, $this->root) === 0);

		return $n;
	}

	/**
	 * 清除过期缓存
	 *
	 * @param  int  $timing  清理时间间隔(秒)
	 *
	 * @return int 成功删除缓存数量
	 */
	public function clear ($timing = 60 * 60) {
		$n = 0;
		if ((null !== $this->get($this->key_clear)) || (false === $files = glob($this->dir . '{,.}*.php', GLOB_BRACE)))
			return $n;

		foreach ($files as $file) {
			$name = basename($file);
			if ('.' === $name || '..' === $name || is_dir($file)) continue;

			$mtime = filemtime($file);
			if ($mtime === 0 || $mtime >= time()) continue;

			if (@unlink($file) || (chmod($file, 0777) && @unlink($file))) $n++;
		}

		return $this->set($this->key_clear, $timing, $timing) ? $n : 0;
	}

	/**
	 * @param  string  $key
	 *
	 * @return string
	 */
	private function key2name ($key) {
		$md5 = $this->md5;
		if ($key === $this->key_clear) $md5 = false;
		else if (is_string($this->prefix)) $key = $this->prefix . $key;

		return $md5 ? md5($key) : preg_replace('#[\\\\/:*?"<>|]#', '_', $key);
	}

	private static function slash_dir ($dir, ...$paths) {
		$arr = preg_split('#[\\\\/]+#', $dir);

		$paths   = array_merge($arr, $paths);
		$i       = 0;
		$paths   = array_map(function ($v) use (&$i) {
			if ($i > 0 || !preg_match('#^[a-zA-Z]+:$#', $v))
				$v = preg_replace('#[:*?"<>|]+#', '_', $v);
			$i++;

			return $v;
		}, $paths);
		$paths[] = '';

		return preg_replace('#[\\\\/]+#', '/', implode('/', $paths));
	}
}

/*$cache = Cache::init()->dir('debug')->md5(false);
//var_dump($cache->set('cs', ['v' => '测试'], 10));
var_dump($cache->get('cs'));
//Cache::clear();*/